Getting Your Halo Infinite Service Record Directly From The API
Table of Contents
Overview #
One of the more interesting data points for any Halo player is generally their own service record. That is - a high-level snapshot of their historical performance in the game. This has been one of the staples of the Halo multiplayer experience for years, and Halo Infinite is no exception to that. Anyone who plays the game is likely already familiar with what you can see inside Halo Waypoint:
A more “concise” version of the service record can also be seen in-game:
Now, if you’ve been reading this blog for some time now, you likely have an idea of the fact that I am rarely satisfied with the defaults, so I started digging through the API to programatically keep track of my service record changes. Part of this work is also embedded in what I am building with OpenSpartan:
The basic service record API #
I’ll preface by saying that to get the service record, the API consumer needs to be authenticated. You can significantly simplify this process (I wrote the original article almost two years ago) by using the Microsoft Authentication Library (MSAL) and a public client application. This will eliminate the tedious process of trying to jump through the hoops of authentication codes and secrets where they don’t need to exist in the first place.
To actually get the service record, one would send a GET HTTP request with the X-343-Authorization-Spartan
header containing the Spartan V4 token to the following endpoint:
https://halostats.svc.halowaypoint.com
/hi
/players
/{YOUR_GAMERTAG_HERE}
/Matchmade
/servicerecord
?seasonId=Csr/Seasons/CsrSeason5-1.json
Wait, wait, wait... Am I correct in understanding that I need to specify my gamertag and not the Xbox ID (also known as XUID) into the API call?
Yes, indeed - you can replicate this by looking inside the Network tab of your favorite web browser when you try to look inside your own service record on Halo Waypoint. However, you can still use a XUID here if you do know it for a player, like this:
https://halostats.svc.halowaypoint.com
/hi
/players
/xuid({YOUR_XUID_HERE})
/Matchmade
/servicerecord
?seasonId=Csr/Seasons/CsrSeason5-1.json
The returned data is absolutely identical, and it’s mostly a matter of preference and scenario as to what value you’d want to use here.
While we’re looking at the request itself, let’s also take a moment to acknowledge Csr/Seasons/CsrSeason5-1.json
. This is an optional query string that can be used to specify the season for which the service record data is returned. This is extremely convenient if, for example, you’d want to analyze how you’re performing in, say, Season 5: Reckoning, while excluding everything else. If you omit it, you will get the overall stats for the player that include every single season the player has been active in.
But where exactly do you get the value for the seasonId query parameter? Is this something one would need to guess based on past seasons?
Not really - this job actually becomes much easier once you run the request above without the seasonId
parameter, because that will in turn give you a list of seasons where the player was active, like this:
"SeasonIds": [
"Seasons/Season6.json",
"Seasons/Season6-2.json",
"Seasons/Season7.json",
"Seasons/Season-Winter-Break-22.json",
"Seasons/Season3.json",
"Seasons/Season4.json",
"Csr/Seasons/CsrSeason5-1.json"
]
The naming is a bit inconsistent - I haven’t actually played in Season 6 or Season 7. Those are just the numeric values for previous seasons that are out-of-sync with the reality. This is generally not a problem since these values are never actually seen by the public and are just used to fetch seasonal metadata, but that’s what we get for looking under the hood. Additional season information (such as season paths) is captured through another data file - seasoncalendar.json
, available at:
https://gamecms-hacs.svc.halowaypoint.com
/hi
/progression
/file
/calendars
/seasons
/seasoncalendar.json
A GET
request here would return data like this, giving you a hint of available season IDs:
{
"Seasons": [
{
"CsrSeasonFilePath": "Csr/Seasons/CsrSeason1-2.json",
"OperationTrackPath": "RewardTracks/Operations/battlepass-noblesacrifice.json",
"SeasonMetadata": "Seasons/Season1.json",
"StartDate": {
"ISO8601Date": "2021-06-20T17:00:00.000Z"
},
"EndDate": {
"ISO8601Date": "2022-05-03T17:00:00.000Z"
}
},
{
"CsrSeasonFilePath": "Csr/Seasons/CsrSeason2-3.json",
"OperationTrackPath": "RewardTracks/Operations/battlepass-lonewolves.json",
"SeasonMetadata": "Seasons/Season2.json",
"StartDate": {
"ISO8601Date": "2022-05-03T17:00:00.000Z"
},
"EndDate": {
"ISO8601Date": "2022-11-08T17:00:00.000Z"
}
},
{
"CsrSeasonFilePath": "Csr/Seasons/CsrSeason2-3.json",
"OperationTrackPath": "RewardTracks/Operations/battlepass-WinterBreak.json",
"SeasonMetadata": "Seasons/Season-Winter-Break-22.json",
"StartDate": {
"ISO8601Date": "2022-11-08T17:00:00.000Z"
},
"EndDate": {
"ISO8601Date": "2023-03-07T17:00:00.000Z"
}
},
{
"CsrSeasonFilePath": "Csr/Seasons/CsrSeason3-1.json",
"OperationTrackPath": "RewardTracks/Operations/S03BattlePass.json",
"SeasonMetadata": "Seasons/Season3.json",
"StartDate": {
"ISO8601Date": "2023-03-07T17:00:00.000Z"
},
"EndDate": {
"ISO8601Date": "2023-06-20T17:00:00.000Z"
}
},
{
"CsrSeasonFilePath": "Csr/Seasons/CsrSeason4-1.json",
"OperationTrackPath": "RewardTracks/Operations/S04BattlePass.json",
"SeasonMetadata": "Seasons/Season4.json",
"StartDate": {
"ISO8601Date": "2023-06-20T17:00:00.000Z"
},
"EndDate": {
"ISO8601Date": "2023-10-17T17:00:00.000Z"
}
},
{
"CsrSeasonFilePath": "Csr/Seasons/CsrSeason5-1.json",
"OperationTrackPath": "RewardTracks/Operations/S05OpPassL01.json",
"SeasonMetadata": "Seasons/Season5-Op1.json",
"StartDate": {
"ISO8601Date": "2023-10-17T17:00:00.000Z"
},
"EndDate": {
"ISO8601Date": "2023-11-14T17:00:00.000Z"
}
},
{
"CsrSeasonFilePath": "Csr/Seasons/CsrSeason5-1.json",
"OperationTrackPath": "RewardTracks/Operations/S05OpPassM01.json",
"SeasonMetadata": "Seasons/Season5-Op2.json",
"StartDate": {
"ISO8601Date": "2023-11-14T17:00:00.000Z"
},
"EndDate": {
"ISO8601Date": "2023-12-19T17:00:00.000Z"
}
},
{
"CsrSeasonFilePath": "Csr/Seasons/CsrSeason5-1.json",
"OperationTrackPath": "RewardTracks/Operations/S05OpPassM02.json",
"SeasonMetadata": "Seasons/Season5-Op3.json",
"StartDate": {
"ISO8601Date": "2023-12-19T17:00:00.000Z"
},
"EndDate": {
"ISO8601Date": "2024-01-30T17:00:00.000Z"
}
}
],
"Events": [...],
"CareerRank": {
"RewardTrackPath": "RewardTracks/CareerRanks/careerrank1.json"
}
}
Or, alternatively, if you do not want to get past events (which are now transitioned to Operations), you can request:
https://gamecms-hacs.svc.halowaypoint.com
/hi
/Progression
/file
/Csr
/Calendars
/CsrSeasonCalendar.json
This will give you an idea of how long “ranked” seasons last, which, so far, map 1:1 to the actual season length. Notice that from Season 5 onward, CsrSeasonFilePath
actually is consistent and is the same for the seasonId
parameter in the service record request. Prior to that, the Csr/Season/CsrSeasonN-N.json
files were separate, and using them when querying your service record would yield no useful data at all.
This also reminds me that we should talk a bit about the structure of the response. The layout of the data resembles this format:
Wow that was a lot. If you prefer to look at a JSON version, here is my own recent service record snapshot. The service record here returns quite a bit of information related to in-game performance (across all supported game modes, like Capture The Flag, Extraction, Infection, and more), medals, as well as breakdown of personal scores. The cryptic GameVariantCategories
represents the game variants in which the player participated. In the JSON representation, we get numbers, but if we request the XML version of the exact same service record (with Accept: application/xml
header attached), we quickly learn that each value has a name, like this:
<d3p1:GameVariantCategory>MultiplayerFiesta</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerBastion</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerStrongholds</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerSlayer</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerExtraction</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerOddball</d3p1:GameVariantCategory>
<d3p1:GameVariantCategory>MultiplayerCtf</d3p1:GameVariantCategory>
Looking into every single value is going to be saved for another post. PlaylistAssetIds
, in turn, represents each playlist in which the player participated during the season. IsRanked
hints at whether the player has acquired a rank during a given season.
From that entire list, this likely is going to be the most confusing one - what the heck does CoreStats > PersonalScores
represent? The data structures for each personal score is basically the same as for medals, as we’ve seen through this static file:
https://gamecms-hacs.svc.halowaypoint.com
/hi
/Waypoint
/file
/medals
/metadata.json
There is no such metadata file that we see from existing requests for personal scores. However, we can kind of deduce what the points are awarded for if we look at some of the match details and try to correlate them with the NameId
values. For example, here is one of my recent Husky Raid (CTF) match performances:
The personal scores for this same match, also for me, are as follows:
"PersonalScores": [
{
"NameId": 1024030246,
"Count": 15,
"TotalPersonalScoreAwarded": 1500
},
{
"NameId": 152718958,
"Count": 9,
"TotalPersonalScoreAwarded": 90
},
{
"NameId": 638246808,
"Count": 10,
"TotalPersonalScoreAwarded": 500
},
{
"NameId": 1267013266,
"Count": 2,
"TotalPersonalScoreAwarded": 20
},
{
"NameId": 601966503,
"Count": 1,
"TotalPersonalScoreAwarded": 300
},
{
"NameId": 249491819,
"Count": 1,
"TotalPersonalScoreAwarded": -100
}
],
From here, we can relatively easily draw the following parallels:
Name ID | Points Per Award | Times Awarded | Name |
---|---|---|---|
1024030246 |
100 | 15 | Kills |
152718958 |
10 | 9 | Callout Assists |
638246808 |
50 | 10 | Assists |
1267013266 |
10 | 2 | Flag Secures |
601966503 |
300 | 1 | Flag Captures |
249491819 |
-100 | -100 | Self-Destructions |
Notice that the Flag Secures personal score is not really extrapolated from the visible stats. Instead, it’s inferred from the CaptureTheFlagStats
property (since Husky Raid is, after all, chaotic Capture The Flag):
"CaptureTheFlagStats": {
"FlagCaptureAssists": 0,
"FlagCaptures": 1,
"FlagCarriersKilled": 0,
"FlagGrabs": 6,
"FlagReturnersKilled": 0,
"FlagReturns": 0,
"FlagSecures": 2,
"FlagSteals": 0,
"KillsAsFlagCarrier": 0,
"KillsAsFlagReturner": 0,
"TimeAsFlagCarrier": "PT6.9S"
}
Similar approaches can be taken with other data points, and because personal score NameId
values are not really captured in a single accessible file, we’ll have to do some manual work to understand them, if we ever were in a position where we need to make those a bit more human-readable.
Player rank #
If you look through the content above, you will notice that the service record barely talks about the player rank. We know that someone is ranked but what is their specific rank? That data is captured through the skill
endpoint:
https://skill.svc.halowaypoint.com
/hi
/playlist
/{RANKED_PLAYLIST_ID}
/csrs?players={YOUR_XUID}
For example, for the Ranked Doubles playlist (with the ID equal to fa5aa2a3-2428-4912-a023-e1eeea7b877c
), the response for my player will be:
{
"Value": [
{
"Id": "MY_XUID",
"ResultCode": 0,
"Result": {
"Current": {
"Value": 997,
"MeasurementMatchesRemaining": 0,
"Tier": "Platinum",
"TierStart": 950,
"SubTier": 1,
"NextTier": "Platinum",
"NextTierStart": 1000,
"NextSubTier": 2,
"InitialMeasurementMatches": 5,
"DemotionProtectionMatchesRemaining": 0,
"InitialDemotionProtectionMatches": 3
},
"SeasonMax": {
"Value": 1005,
"MeasurementMatchesRemaining": 0,
"Tier": "Platinum",
"TierStart": 1000,
"SubTier": 2,
"NextTier": "Platinum",
"NextTierStart": 1050,
"NextSubTier": 3,
"InitialMeasurementMatches": 5,
"DemotionProtectionMatchesRemaining": 0,
"InitialDemotionProtectionMatches": 3
},
"AllTimeMax": {
"Value": 1005,
"MeasurementMatchesRemaining": 0,
"Tier": "Platinum",
"TierStart": 1000,
"SubTier": 2,
"NextTier": "Platinum",
"NextTierStart": 1050,
"NextSubTier": 3,
"InitialMeasurementMatches": 5,
"DemotionProtectionMatchesRemaining": 0,
"InitialDemotionProtectionMatches": 3
}
}
}
]
}
CSR stands for Competitive Skill Rank. With the data above, you can see data not only for yourself, but also for other players, the XUIDs for which can be chained in the request under players
. You will be able to see their current rank, maximum rank for the season, as well as the all-time maximum across their entire career.
As an added bonus, you can constrain the rank lookup to a given season with the season
query argument:
https://skill.svc.halowaypoint.com
/hi
/playlist
/{RANKED_PLAYLIST_ID}
/csrs?players={YOUR_XUID}&season={SEASON_ID}
The {SEASON_ID}
is a simplified season identifier, as seen from the values above, like CsrSeason5-1
(without the path prefixes).
As an aside, remember how I talked about playlist weights in a previous post? You can use that information to get more details about the ranked playlist through the discovery
endpoint, like this (for Ranked Doubles mentioned above):
https://discovery-infiniteugc.svc.halowaypoint.com
/hi
/Playlists
/fa5aa2a3-2428-4912-a023-e1eeea7b877c
/versions
/3da879ff-1df5-40c9-973f-97202d522288
Or, for an abridged version:
https://gamecms-hacs.svc.halowaypoint.com
/hi
/Multiplayer
/file
/playlists
/assets
/fa5aa2a3-2428-4912-a023-e1eeea7b877c.json
Conclusion #
With the two API endpoints above (service record and CSR) you can get regular snapshots of your own in-game performance. That’s what I use to populate my very own Halo stats page on this blog.